#!/usr/bin/python2
# -*- coding: utf-8 -*-

from __future__ import division
import copy
from operator import attrgetter
import logging
import time
import os
import sys
import random

from eventlet import sleep

from ryu.ofproto import ofproto_v1_3

import setting
from DemandEstimation import demand_estimation
from ryuapp.lib.hedera.FatTreeSim import Awareness
from updatewrapper import UpdateWrapper

class Hedera:

    def __init__(self, update_queue, fattreesim, frequency):
        self.__update_queue = update_queue
        self.__period = 1.0/frequency

        self.awareness = Awareness(fattreesim)
        self.datapaths = {id:id for id in fattreesim.nodes}

        self.port_stats = {}
        self.port_speed = {}
        self.flow_stats = {}
        self.flow_speed = {}

        self.port_features = {}
        self.free_bandwidth = {}  # {dpid:{port_no:free_bw,},} Unit:Kbit/s

        self.hostsList = []
        self.flows = []  # Record flows that need to be rescheduled. (hmc)
        self.statRecord = []

        self.logger = logging.getLogger("NetworkSim")
        self.fattreesim = fattreesim

    def run(self):
        endtime = time.time() + 160
        sim_period = 0.025
        # next_restart = time.time()+13
        while True:
            period_end = time.time() + self.__period
            while True:
                t = time.time()
                if t >= period_end:
                    break
                # if t >= next_restart:
                #     next_restart = t+13
                #     self.__restart()
                self.fattreesim.update(sim_period)
                dt = time.time() - t
                if dt < sim_period:
                    sleep(sim_period - dt)
                    print("sleep")
            if time.time() >= endtime:
                break

            self.statRecord = []
            self.flows = []
            if True:
                for dp in self.datapaths:
                    self.port_features.setdefault(dp, {})
                    self._port_desc_stats_reply_handler(
                        self.fattreesim.mock_port_desc(dp)
                    )
                    self._port_stats_reply_handler(
                        self.fattreesim.mock_port_stats(dp)
                    )
                self.mock_flow_estimate()
            print("monitor period over %f" % time.time())
        self.fattreesim.show()

        sys.stdout.flush()
        os._exit(0)

    def __restart(self):
        update = {}
        for pair,path in self.fattreesim.installed_path.iteritems():
            ps = self.awareness.shortest_paths(*pair)
            if path!=ps[0]:
                update[pair] = ps[0]
        if update:
            self.__update_queue.put(UpdateWrapper(update))


    def distributed_run(self):
        count = 400
        while True:
            self.statRecord = []
            self.flows = []
            for i in range(int(setting.MONITOR_PERIOD/0.1)):
                t1 = time.time()
                self.fattreesim.update(0.1)
                dt = time.time()-t1
                if dt<0.1:
                    # time.sleep(0.2-dt)
                    pass
                count-=1
                if count<=0:
                    break
            if count<=0:
                break
            if setting.HEDERA:
                for dp in self.datapaths:
                    self.port_features.setdefault(dp, {})
                    self._port_desc_stats_reply_handler(
                        self.fattreesim.mock_port_desc(dp)
                    )
                    self._port_stats_reply_handler(
                        self.fattreesim.mock_port_stats(dp)
                    )
                self.mock_flow_estimate()
            print("monitor period over %f"%time.time())
        self.fattreesim.show()
        os._exit(0)

    def _port_stats_reply_handler(self, ev):
        dpid = ev.dpid
        body = ev.body

        # self.stats['port'][dpid] = body
        self.free_bandwidth.setdefault(dpid, {})
        for stat in sorted(body, key=attrgetter('port_no')):
            port_no = stat.port_no
            if port_no != ofproto_v1_3.OFPP_LOCAL:
                key = (dpid, port_no)
                value = (stat.tx_bytes, stat.rx_bytes, stat.rx_errors,
                         stat.duration_sec, stat.duration_nsec)
                self._save_stats(self.port_stats, key, value, 5)
                # Get port speed and Save it.
                pre = 0
                period = setting.MONITOR_PERIOD
                tmp = self.port_stats[key]
                if len(tmp) > 1:
                    # Calculate only the tx_bytes, not the rx_bytes. (hmc)
                    pre = tmp[-2][0]
                    period = self._get_period(tmp[-1][3], tmp[-1][4], tmp[-2][3], tmp[-2][4])
                speed = self._get_speed(self.port_stats[key][-1][0], pre, period)
                self._save_stats(self.port_speed, key, speed, 5)
                self._save_freebandwidth(dpid, port_no, speed)

    def _port_desc_stats_reply_handler(self, ev):
        dpid = ev.dpid
        for p in ev.body:
            config = 'up'
            state = 'up'
            # Recording data.
            port_feature = (config, state, p.curr_speed)
            self.port_features[dpid][p.port_no] = port_feature

    def mock_flow_estimate(self):
        self.flows,self.hostsList = self.fattreesim.mock_flows_to_schedule()
        flows = sorted([flow for flow in self.flows], key=lambda flow: (flow['src'], flow['dst']))
        # hostsList = sorted(self.hostsList)
        random.shuffle(self.hostsList)
        hostsList = self.hostsList
        self._demandEstimator(flows, hostsList)

    def _demandEstimator(self, flows, hostsList):
        estimated_flows = demand_estimation(flows, hostsList)
        self.__one_update = {}
        for flow in estimated_flows:
            if flow['demand'] > 0.05:
                self._GlobalFirstFit(flow)
        # update
        if self.__one_update:
            self.__update_queue.put(UpdateWrapper(self.__one_update))

    def _GlobalFirstFit(self, flow):
        src_dp = flow['src']
        dst_dp = flow['dst']
        paths = self.awareness.shortest_paths(src_dp,dst_dp)

        GFF_route = None
        for path in paths:
            fitCheck = True
            for i in xrange(len(path) - 1):
                fitCheck = False
                src_port,_ = self.awareness.link_to_port(path[i], path[i + 1])
                if src_port:
                    src_port = src_port[1]
                    if self.free_bandwidth.has_key(path[i]) and self.free_bandwidth[path[i]].has_key(src_port):
                        if (self.free_bandwidth[path[i]][src_port] / setting.MAX_CAPACITY) < flow['demand']-0.05:
                            break
                        else:
                            fitCheck = True
                    else:
                        raise RuntimeError()

            if fitCheck == True:
                GFF_route = path
                for i in range(len(path)-1):
                    src_port, _ = self.awareness.link_to_port(path[i], path[i + 1])
                    src_port = src_port[1]
                    self.free_bandwidth[path[i]][src_port] -= flow['demand'] * setting.MAX_CAPACITY
                self.logger.info("[GFF PATH]%s<-->%s: %s" % (flow['src'], flow['dst'], path))
                break
        if GFF_route:
            # Install new GFF_path flow entries.
            print("[GFF INSTALLING]%s<-->%s: %s" % (flow['src'], flow['dst'], path))
            self._install_GFF_path(GFF_route, flow['match'], flow['priority'])

    def _install_GFF_path(self, GFF_route, match, priority):
        self.__one_update[match] = GFF_route


    def _save_freebandwidth(self, dpid, port_no, speed):
        port_state = self.port_features.get(dpid).get(port_no)
        if port_state:
            capacity = setting.MAX_CAPACITY  # The true bandwidth of link, instead of 'curr_speed'.
            free_bw = self._get_free_bw(capacity, speed)
            self.free_bandwidth[dpid].setdefault(port_no, None)
            self.free_bandwidth[dpid][port_no] = free_bw
        else:
            self.logger.info("Port is Down")

    def _save_stats(self, _dict, key, value, length=5):
        if key not in _dict:
            _dict[key] = []
        _dict[key].append(value)
        if len(_dict[key]) > length:
            _dict[key].pop(0)

    def _get_speed(self, now, pre, period):
        if period:
            return (now - pre) / (period)
        else:
            return 0

    def _get_free_bw(self, capacity, speed):
        return max(capacity - speed * 8 / 1000.0, 0)

    def _get_time(self, sec, nsec):
        return sec + nsec / 1000000000.0

    def _get_period(self, n_sec, n_nsec, p_sec, p_nsec):
        return self._get_time(n_sec, n_nsec) - self._get_time(p_sec, p_nsec)

